<?php
namespace DRY\Serializer;

/**
 * DonReY Framework 2012 :: Serializer Algorithm - Comma-separated values ( http://en.wikipedia.org/wiki/Comma-separated_values )
 * Converts a CSV format string ( multiple rows ) into a 2-dimensional array which looks similar to a database result (and vice-versa)
 * Conforms to RFC4180 (http://tools.ietf.org/html/rfc4180), however it can also use or generate a "header" ( First line in CSV containing field names )
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/serializer.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Serializer
 */
class Algorithm_Csv implements Algorithm_base
{
	/**
	 * Adds quotes on values containing commas, newlines and quotes
	 * @param string $value
	 */
	private static function escape_Value(&$value)
	{
		$value = (string)$value;
		if(strpos($value, '"')!==false||strpos($value, ',')!==false||strpos($value, "\r")!==false||strpos($value, "\n")!==false) {
			// convert simple to double quotes inside a string, and enclose in quotes
			$value = '"'.str_replace('"', '""', $value).'"';
		}
	}

	/**
	 * Create a CSV encoded table, optionally using a header row.
	 * In header mode, to determine which fields are used, it scans the entire array before generating the CSV, and uses all the found array keys.
	 * @see DRY_Serialize_Algorithm_base::serialize()
	 */
	public static function serialize($data, $header = false)
	{
		$csv = '';
		if($header) {
			// construct a list with all the keys used (numeric or not)
			$keys = array(); $combine_temp = array();
			foreach($data as $row) {
				$combine_temp+= $row;
			}
			$header = $keys = array_keys($combine_temp); unset($combine_temp);

//			// write a first line of keys
			array_walk($header, array(__CLASS__, "escape_Value"));
			$csv.= implode(',', $header). PHP_EOL;
		}

		// start writing each row
		foreach($data as $row) {
			if($header) {
				// create a new version of the $row by placing it's values in the proper position, according to the row's key
				$full_row = array();
				foreach($keys as $key)
					$full_row[] = isset($row[$key])?$row[$key]:null;
				$row = $full_row;
			}
			array_walk($row, array(__CLASS__, "escape_Value"));
			$csv.= implode(',', $row). PHP_EOL;
		}
		return $csv;
	}

	// CSV Parser modes (for unserialize only)
	const
		M_DETECT = 0,
		M_SIMPLE = 1,
		M_STRING = 2,
		M_CHECKQUOTE = 3;

	/**
	 * Decodes a CSV string into 2-dimeansional array, optionally using first row as header
	 * When using a header row, all the rest of rows must have the exact number of columns as the header
	 * @see DRY_Serialize_Algorithm_base::unserialize()
	 */
	public static function unserialize(&$str, $header = false)
	{
		// Can't do this with Regex, maybe someone smarter than me can... so, good old string navigating method:

		$len = strlen($str);
		$arr = array(); $row = array(); $value = ''; $mode = self::M_DETECT; $eor = false; $keys = array();

		for($c = 0; $c< $len; $c++) {
			// detect character
			switch($str[$c]) {
				case '"':
					switch($mode) {
						case self::M_DETECT: // at the beginning
							$value = ''; $mode = self::M_STRING; break;
						case self::M_STRING: // inside a string, check if end-of-string or double-quote inside string
							$mode = self::M_CHECKQUOTE; break;
						case self::M_CHECKQUOTE: // quote encountered while checking quote - Enclosed quote detected
							$value.= '"'; $mode = self::M_STRING; break;
						case self::M_SIMPLE:
							return false; // error, after "simple" mode started, double-quote can't appear
					}; break;
				case "\r": // end of line
				case "\n":
					$eor = true;
				case ',': // separator encountered, end of value
					switch($mode) {
						case self::M_DETECT: // empty value, mode is unchanged
							$row[] = null;
							$value = ''; break;
						case self::M_SIMPLE:
							// some type coversions
							if(is_numeric($value)) {
								if($value == (int)$value)
									$value = (int)$value;
								elseif($value == (float)$value)
									$value = (float)$value;
							}
							$row[] = $value;
							$value = ''; $mode = self::M_DETECT; break;
						case self::M_STRING: // inside strings, a comma or end of line will be part of the value, just like any other character
							$value.= $str[$c]; $eor = false; continue(2);
						case self::M_CHECKQUOTE: // comma after encountering a quote inside a string, this is the end of string
							$row[] = $value; // always string
							$value = ''; $mode = self::M_DETECT; break;
					};
					if($c+1 < $len && $str[$c+1] == "\n") { // double-character termination
						if($mode == self::M_STRING)
							$value.="\n";
						$c++; // skip it
					};
					// end of row ( encountered End-of-Line separator which is not inside a string )
					if($eor) {
						$eor = false;
						if($header) { // first row is header, not part of the data itself
							$keys = $row;
							$header = false;
						} else {
							if(count($keys)>0) {
								if(count($row) != count($keys)) {
									\DRY\Water::dump(array('Row'=> $row, 'Row length'=>count($row), 'Header length'=>count($keys)), "Serialize_Algorithm_Csv::Row of different length than the header", null, 'warning');
									return false; // Error, some row is of different length, error in CSV-with-header format
								}
								$arr[] = array_combine($keys, $row);
							} else
								$arr[] = $row;
						}
						$row = array();
					}
					break;
				default: // something not special in any way
					switch($mode) {
						case self::M_CHECKQUOTE:
							return false; // standard character after string closing
						case self::M_DETECT:
							$mode = self::M_SIMPLE;
						default:
							$value.= $str[$c];
					}

			}


		}
		return $arr;
	}

}